/*
 * ====================================================================
 * Copyright (C) 2013 Eric Blond
 *
 * This file is part of Neptune.
 *
 * Neptune is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Neptune is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Neptune.  If not, see <http://www.gnu.org/licenses/>.
 * ====================================================================
 */

package com.hyperplan.neptune.lexer;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

import com.hyperplan.neptune.utils.Location;

public class Lexer {

	private static String readFile(Path path) throws IOException {
		FileInputStream stream = new FileInputStream(new File(path.toString()));
		try {
			FileChannel fc = stream.getChannel();
		    MappedByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
		    /* Instead of using default, pass in a decoder. */
		    return Charset.defaultCharset().decode(bb).toString();
		}
		finally {
		    stream.close();
		}
	}
	
	private static boolean isIdentifierFirstChar(char c) {
		return Character.isLetter(c) || (c == '_');
	}
	
	private static boolean isIdentifierOtherChar(char c) {
		return Character.isLetter(c) || Character.isDigit(c) || (c == '-');
	}
	
	private static final char EOF = '\u001a';
	
	private static String displayCharacter(char character) {
		String representation = String.valueOf(character).replaceAll("\\p{C}", "?");
		return String.format("'%s' (0x%X)", representation, (int)character);
	}
	
	private static boolean lookahead(String content, int index, String pattern) {
		for (int i = 0; i < pattern.length(); i++) {
			if ((index + i) >= content.length()) {
				return false;
			}
			if (content.charAt(index + i) != pattern.charAt(i)) {
				return false;
			}
		}
		return true;
	}
	
	public static List<Token> run(Path path) throws IOException, LexerException {
		String content = readFile(path);
		Stack<ContainerToken> token_stack = new Stack<ContainerToken>();
		Stack<Integer> indent_stack = new Stack<Integer>();
		indent_stack.add(0);
		int currentIndex = 0;
		int currentLine = 1;
		int currentColumn = 1;
		int markLine = 0;
		int markColumn = 0;
		LexerState state = LexerState.ADD_INDENT;
		StringBuffer buffer = null;
		IdentifierKind identifierKind = null;
		while (currentIndex <= content.length()) {
			char character = (currentIndex < content.length())
					? content.charAt(currentIndex) : EOF;
			switch (state) {
			case NORMAL:
			{
				SymbolKind symbolKind = null;
				switch (character) {
				case ':':
					symbolKind = SymbolKind.COLON;
					state = LexerState.AFTER_COLON;
					break;
				
				case '~':
					state = LexerState.IDENTIFIER;
					identifierKind = IdentifierKind.CLASS;
					markLine = currentLine;
					markColumn = currentColumn;
					currentColumn += 1;
					break;

				case '@':
					state = LexerState.IDENTIFIER;
					identifierKind = IdentifierKind.PAGE;
					markLine = currentLine;
					markColumn = currentColumn;
					currentColumn += 1;
					break;

				case '&':
					state = LexerState.IDENTIFIER;
					identifierKind = IdentifierKind.ENUM;
					markLine = currentLine;
					markColumn = currentColumn;
					currentColumn += 1;
					break;

				case '\r':
				case ' ':
				case '\t':
					currentColumn += 1;
					break;
				
				case '\n':
					currentLine += 1;
					currentColumn = 1;
					state = LexerState.READ_INDENT;
					break;
				
				case '"':
					buffer = new StringBuffer();
					markLine = currentLine;
					markColumn = currentColumn;
					if (lookahead(content, currentIndex, "\"\"\"")) {
						state = LexerState.MULTILINE_STRING;
						currentColumn += 3;
						currentIndex += 2;
					} else {
						state = LexerState.STRING;
						currentColumn += 1;
					}
					break;
				
				case EOF:
					break;
					
				default:
					for (SymbolKind kind : SymbolKind.values()) {
						if (lookahead(content, currentIndex, kind.getText())) {
							symbolKind = SymbolKind.PERCENT;
							break;
						}
					}
					if (symbolKind == null) {
						if (isIdentifierFirstChar(character)) {
							state = LexerState.IDENTIFIER;
							identifierKind = IdentifierKind.VALUE;
							buffer = new StringBuffer();
							buffer.append(character);
							markLine = currentLine;
							markColumn = currentColumn;
							currentColumn += 1;
						} else {
							throw new LexerException(currentLine, currentColumn,
									String.format("Invalid character [%s]",
											displayCharacter(character)));
						}
					}
				}
				if (symbolKind != null) {
					token_stack.peek().getElements().add(new SymbolToken(
							new Location(currentLine, currentColumn, 1), symbolKind));
					currentColumn += 1;
					symbolKind = null;
				}
				currentIndex += 1;
			}
			break;
				
			case IDENTIFIER:
				if (buffer == null) {
					if (isIdentifierFirstChar(character)) {
						buffer = new StringBuffer();
						buffer.append(character);
						currentColumn += 1;
						currentIndex += 1;
					} else {
						throw new LexerException(currentLine, currentColumn - 1,
								"Empty identifier");
					}
				} else {
					if (isIdentifierOtherChar(character)) {
						buffer.append(character);
						currentColumn += 1;
						currentIndex += 1;
					} else {
						if (character == '!') {
							identifierKind = IdentifierKind.PROC;
							currentColumn += 1;
							currentIndex += 1;
						}
						String value = buffer.toString();
						Location location = new Location(
								markLine, markColumn, value.length());
						KeywordKind keywordKind = null;
						if (identifierKind == IdentifierKind.VALUE) {
							for (KeywordKind kind : KeywordKind.values()) {
								if (value.equals(kind.getText())) {
									keywordKind = kind;
									break;
								}
							}
						}
						if (keywordKind != null) {
							token_stack.peek().getElements().add(new KeywordToken(
									location, keywordKind));
						} else {
							token_stack.peek().getElements().add(new IdentifierToken(
									location, identifierKind, value));	
						}
						state = LexerState.NORMAL;
					}
				}
				break;
				
			case STRING:
				switch (character) {
				case '\\':
					state = LexerState.ESCAPED_WITHIN_STRING;
					break;
					
				case '\r':
				case '\n':
				case '\t':
					throw new LexerException(currentLine, currentColumn,
							String.format("Unexpected character [%s]",
									displayCharacter(character)));
					
				case '"':
					token_stack.peek().getElements().add(new StringToken(
							new Location(markLine, markColumn, currentLine, currentColumn),
							buffer.toString()));
					buffer = null;
					state = LexerState.NORMAL;
					break;
				
				case EOF:
					throw new LexerException(markLine, markColumn,
							"Missing string end delimiter");
					
				default:
					buffer.append(character);
				}
				currentIndex += 1;
				currentColumn += 1;
				break;
				
			case MULTILINE_STRING:
				switch (character) {
				case '\n':
					currentLine += 1;
					currentColumn = 1;
					break;
					
				case EOF:
					throw new LexerException(markLine, markColumn,
							"Missing string end delimiter");
				
				default:
					currentColumn += 1;
				}
				if (lookahead(content, currentIndex, "\"\"\"")) {
					token_stack.peek().getElements().add(new StringToken(
							new Location(markLine, markColumn, currentLine, currentColumn + 2),
							buffer.toString()));
					buffer = null;
					state = LexerState.NORMAL;
					currentColumn += 2;
					currentIndex += 3;
				} else {
					buffer.append(character);
					currentIndex += 1;
				}
				break;
				
			case ESCAPED_WITHIN_STRING:
				switch (character) {
				case 'r':
					buffer.append('\r');
					break;
					
				case '\\':
					buffer.append('\\');
					break;
					
				case 'n':
					buffer.append('\n');
					break;
					
				case 't':
					buffer.append('\t');
					break;
					
				case 'b':
					buffer.append('\b');
					break;
					
				case 'f':
					buffer.append('\f');
					break;
					
				case '"':
					buffer.append('"');
					break;
					
				case EOF:
					throw new LexerException("Unexpected EOF");
					
				default:
					throw new LexerException(currentLine, currentColumn,
							String.format("Unexpected character [%s]",
									displayCharacter(character)));
				}
				currentIndex += 1;
				currentColumn += 1;
				break;
				
			case AFTER_COLON:
				switch (character) {
				case '\r':
				case ' ':
				case '\t':
					currentIndex += 1;
					currentColumn += 1;
					break;
				
				case '\n':
					state = LexerState.ADD_INDENT;
					currentIndex += 1;
					currentLine += 1;
					currentColumn = 1;
					break;
				
				case EOF:
					throw new LexerException("Unexpected EOF");
					
				default:
					throw new LexerException(currentLine, currentColumn,
							String.format("Unexpected character [%s]",
									displayCharacter(character)));
				}
				break;
				
			case READ_INDENT:
				switch (character) {
				case '\r':
				case ' ':
				case '\t':
					currentIndex += 1;
					currentColumn += 1;
					break;
				
				case '\n':
					currentIndex += 1;
					currentLine += 1;
					currentColumn = 1;
					break;
				
				default:
					while (currentColumn < indent_stack.peek()) {
						indent_stack.pop();
						Token token = token_stack.pop();
						token_stack.peek().getElements().add(token);
					}
					if (currentColumn > indent_stack.peek()) {
						throw new LexerException(currentLine, currentColumn,
								"Bad indentation");
					}
					state = LexerState.NORMAL;
				}
				break;
				
			case ADD_INDENT:
				switch (character) {
				case '\r':
				case ' ':
				case '\t':
					currentIndex += 1;
					currentColumn += 1;
					break;
				
				case '\n':
					currentIndex += 1;
					currentLine += 1;
					currentColumn = 1;
					break;
					
				default:
					if (currentColumn <= indent_stack.peek()) {
						throw new LexerException(currentLine, currentColumn,
								"Bad indentation");
					} else {
						state = LexerState.NORMAL;
						token_stack.add(new ContainerToken(new ArrayList<Token>()));
						indent_stack.add(currentColumn);
					}
				}
				break;
			}
		}
		while (token_stack.size() > 1) {
			ContainerToken token = token_stack.pop();
			token_stack.peek().getElements().add(token);
		}
		return token_stack.peek().getElements();
	}
}
